package com.test.lucene.index;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.index.StaleReaderException;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.search.Hits;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.Searcher;

import com.subshell.lucene.indexaccess.ILuceneIndexAccessor;
import com.test.lucene.LuceneException;
import com.test.lucene.helper.Constants;

/**
 * Search the Lucene index.
 * 
 * @author Patrick Kimber
 * 
 */
final class IndexSearch {

    /** The logger. */
    private final Log logger = LogFactory.getLog(IndexSearch.class);

    /**
     * Search the Lucene index.
     * 
     * @param indexAccessor
     *            The Lucene index accessor.
     * @param criteria
     *            Search criteria.
     * @return The search results.
     * @throws LuceneException
     *             if search fails
     */
    public List search(final ILuceneIndexAccessor indexAccessor,
            final String criteria) throws LuceneException {
        logger.info("search(" + criteria + ")");
        Searcher searcher = null;
        List result = null;
        try {
            searcher = indexAccessor.getSearcher();
            Hits hits = search(criteria, searcher);
            result = processHits(hits);
        } catch (IOException e) {
            throw new LuceneException("I/O Error: Cannot search repository.", e);
        } catch (org.apache.lucene.search.BooleanQuery.TooManyClauses e) {
            throw new LuceneException(e);
        } finally {
            indexAccessor.release(searcher);
        }
        return result;
    }

    /**
     * Search the Lucene index.
     * <p>
     * Retry the search if we get a stale reader exception.
     * 
     * @param criteria
     *            Search criteria.
     * @param searcher
     *            The Lucene searcher object.
     * @return The search results.
     * @throws IOException
     *             If the search fails.
     * @throws LuceneException
     *             If the search fails.
     */
    private Hits search(final String criteria, final Searcher searcher)
            throws IOException, LuceneException {
        Hits result = null;
        RetryHandler handler = new RetryHandler(this.getClass().getName());
        Exception thrown;
        do {
            logger.debug("search(...) retry:" + handler.getCount());
            try {
                thrown = null;
                result = searcher.search(createQuery(criteria));
            } catch (StaleReaderException e) {
                thrown = e;
            }
        } while (handler.retry(thrown));
        return result;
    }

    /**
     * Create a search query.
     * 
     * @param criteria
     *            Convert this criteria to a search query.
     * @return The search query.
     * @throws LuceneException
     *             If the query cannot be parsed.
     */
    private Query createQuery(final String criteria) throws LuceneException {

        try {
            Query result = new QueryParser(Constants.TITLE,
                    new StandardAnalyzer()).parse(criteria);
            logger.debug("createQuery(" + criteria + ") " + result.toString());
            return result;
        } catch (ParseException e) {
            throw new LuceneException("Cannot parse query.", e);
        }
    }

    /**
     * Convert the search results to a list of {@link DocumentResult} objects.
     * 
     * @param hits
     *            returned from the search
     * @return an array list containing the results.
     * @throws LuceneException
     *             if cannot get the document
     */
    private List processHits(final Hits hits) throws LuceneException {
        logger.debug("processHits(.) hits:" + hits.length());
        List result = new ArrayList();
        for (int i = 0; i < hits.length(); ++i) {
            result.add(getDocument(hits, i));
        }
        return result;
    }

    /**
     * Convert the hit into a map.
     * <ul>
     * <li>key = Field Name
     * <li>value = The contents of the field as a string.
     * </ul>
     * 
     * @param hits
     *            The Lucene search results.
     * @param index
     *            The index of the hit you want converted to a document.
     * @return A map containing the field names and contents.
     * @throws LuceneException
     *             If the document cannot be created.
     */
    private Map getDocument(final Hits hits, final int index)
            throws LuceneException {
        logger.debug("getDocument(," + index + ")");
        Map result = new HashMap();
        try {
            Collection fields = hits.doc(index).getFields();
            for (Iterator i = fields.iterator(); i.hasNext();) {
                org.apache.lucene.document.Field field = (org.apache.lucene.document.Field) i
                        .next();
                result.put(field.name(), field.stringValue());
            }
            // Add hit related data.
            result.put("SCORE", Float.toString(hits.score(index)));
            result.put(Constants.DOCUMENT_NUMBER, Integer.toString(hits
                    .id(index)));
        } catch (IOException e) {
            throw new LuceneException("I/O Error: Cannot get the document.", e);
        }
        return result;
    }
}
